Using shadcn Components

安装组件:pnpm dlx shadcn@latest add button field input select checkbox

在所有form.Field的外层,包裹一个<FieldGroup></FieldGroup>。然后form.Field上除了children的部分,都不需要改变。

form.Field的children部分,可以写成children属性的形式,也可以直接写在form.Field标签之间。

可以看到,使用了<Field></Field>FieldLabelFieldDescriptionFieldError,这部分是通用的。

然后就是具体的交互组件,需要给定id值,id值也就是<form.Field name=""这里的值,反正要一样。然后就是数据的绑定,value、onChange、onBlur。

需要注意的是,不同的交互组件,上面绑定的属性名value、onChange、onBlur不一定是这三种,需要根据实际情况进行处理。可以在这里查询到:https://ui.shadcn.com/docs/forms/tanstack-form

文档案例:

image-20251228140231845

老师案例效果:

image-20251228140432111

Refactoring Our Form & Using useAppForm

从上面的学习,我们可以知道,代码量还是蛮大的,而且有很多可以重复使用的部分。这节课来学习重构form表单。

参考:https://tanstack.com/form/v1/docs/framework/react/guides/form-composition

如何将大型、复杂的表单拆解成可复用、可组合的小型组件,同时保持优秀的类型安全、性能和可维护性。这份指南(最新版本)强调使用自定义 Hook、预绑定组件、HOC 模式等高级模式,避免在实际项目中写一堆重复的 <form.Field> 样板代码。

核心思想与目标

主要组合模式(从简单到高级)

模式名称核心 API适用场景复杂度类型安全推荐指数
自定义 Form HookcreateFormHook + createFormHookContexts整个应用统一的表单风格、预绑定组件★★☆★★★★★★★★★★
预绑定 Field 组件fieldComponents + useFieldContext复用 ★☆☆★★★★★★★★★★
预绑定 Form 组件formComponents + useFormContext统一的提交按钮、loading 状态组件★☆☆★★★★★★★★
用 withForm 拆分子表单withForm HOC把大表单切成小块(个人信息、地址、支付等)★★★★★★★★★★★★★
用 withFieldGroup 复用字段组withFieldGroup HOC密码+确认密码、地址三连(省/市/区)等强相关字段★★★★★★★★★★★
动态加载 + Tree-shakingReact.lazy + Suspense大型应用中按需加载表单组件★★☆★★★★★★★

自定义 Form Hook(最常用起点)

1、定义form和field的上下文

createFormHookContexts 是一个工厂函数,从 @tanstack/react-form 直接导入,它的作用是一次性创建并导出所有必要的 React Context 和 Context 消费 Hook,专门用于后续的自定义表单钩子(createFormHook)和预绑定组件的构建。

简单说:它帮你生成form 和 field 的上下文基础设施,让你后续可以写出类型安全、可复用、性能好的自定义表单组件,避免在每个地方都手动用 useForm + form.Field 写一堆重复代码。

导出变量的作用:

为什么需要它?(核心价值)

  1. 解耦:把 Context 创建和 Hook 生成分离出来,只创建一次,全局复用
  2. 类型安全:useFieldContext() 可以指定泛型,拿到精确的 FieldApi 类型
  3. 性能:基于 TanStack Store 的 context,不会因无关变化导致子组件重渲染
  4. 可扩展:后续在 createFormHook 里可以注入自定义组件、默认校验等
  5. 支持 Tree-shaking / 懒加载:Context 是静态的,很容易结合 React.lazy 做代码分割

2、自定义form hook

createFormHook 是一个工厂函数,它接收一个配置对象,然后返回一个自定义的 useXXXForm Hook(比如 useAppForm、useMyForm 等)。这个自定义 Hook 相比原生的 useForm 有以下超级优势:

进阶配置(常用选项详解)

配置项类型/作用是否必须典型用法示例
fieldContext来自 createFormHookContexts 的 Field Context必须几乎所有自定义 Hook 都需要
formContext来自 createFormHookContexts 的 Form Context必须同上
fieldComponentsRecord<string, React.ComponentType>推荐绑定你的 Input、Select、DatePicker 等
formComponentsRecord<string, React.ComponentType>可选绑定 SubmitButton、ResetButton 等
defaultOptsPartial可选设置全局 validatorAdapter、默认 validators 等
validatorAdapterValidatorAdapter可选(可放 defaultOpts)全局 Zod/Yup/Valibot 等适配器
基本用法

然后全局使用 useAppForm 而不是 useForm,自动带上预设组件。

推荐项目结构

Creating Reusable Field Components

createFormHookfieldComponents属性里面,要传入定义好的Field Components,才能直接使用。也就是<form.Field></form.Field>的children部分。

重点:

案例

1、创建field component

2、在createFormHook里面注册field component

3、在form中使用这个组件

可以对比一下,第一张是之前的代码,第二张是之后的代码,代码简洁多了,可以让我们专心写逻辑代码。

image-20251228152835884

 

image-20251228152842409

Creating Reusable Form Components

重点:

常见 Form Components 类型及重点实现

组件名称重点关注的内容典型代码片段
SubmitButtonisSubmitting / isValid / loading 状态disabled + 文字切换
ResetButtonform.reset() + 确认弹窗(可选)onClick={() => form.reset()}
FormErrorSummary收集所有 touchedErrors 并显示(全局错误汇总)form.state.meta.errors
FormLoadingOverlay当 isSubmitting 时显示遮罩{form.state.isSubmitting && }
FormStatus显示“已保存”“保存中”“有错误”等提示结合 isDirty / isSubmitting / errors

老师案例

1、创建form component

2、注册

3、使用

可以看到,界面正常,交互正常。

Lazy Loading Form Components

在定义createFormHook的时候,我们引入了很多field components和form components,一次性导入这么多组件可能造成性能问题,所以可以使用React.lazy动态导入(Dynamic Import),来“按需加载”,而不是一次性下载所有内容。

1、在定义createFormHook的文件中,使用按需加载

2、必须配合 <Suspense> 使用

在使用这些组件的地方,你必须包裹 Suspense 并在 fallback 中提供加载状态(如 Loading 菊花图),否则 React 会报错。

可以看到,在出现registerForm之前,有一个短暂的loading效果。

 

注意:

建议:通常建议对“大型页面路由”或“超重型第三方库(如 Chart.js, Rich Text Editor)”使用 lazy。对于这种轻量级的 UI 基础组件,通常直接 import 性能反而更好。

Breaking Forms into Smaller Pieces with withForm HOC

在 TanStack Form 中,withForm 是一个非常强大的 HOC(Higher-Order Component,高阶组件),它的核心作用是:把一个“普通的 React 组件”包装成“带有独立表单状态的子表单组件”,让大型复杂表单可以轻松拆分成多个可复用、可独立维护的小型表单模块,同时保持完整的类型安全和表单上下文。

简单来说: withForm 就是 TanStack Form 提供的“子表单工厂”,专门用来解决“一个页面有多个表单区域,但又不想全部塞到一个巨型 useForm 里”的痛点。

withForm 的主要作用与优势

作用具体说明实际价值(为什么值得用)
1. 独立表单状态与默认值每个 withForm 包装的组件都有自己的 defaultValues 和独立的 FormApi子表单可以独立重置、独立校验、独立提交
2. 字段名映射(Field Mapping)支持把父表单的字段路径映射到子表单的局部字段名比如父表单有 user.address.street,子表单里写 street 就行
3. 上下文自动继承自动继承父表单的 validatorAdapter、onSubmit 等配置子表单不用重复写全局配置,保持一致性
4. 类型安全极强子表单的 defaultValues 只用于类型推导,运行时不生效字段名拼错编译期就报错,开发体验极佳
5. 支持嵌套 & 复用可以嵌套使用、跨页面复用,甚至动态加载(lazy)适合大型表单、向导式多步表单、复用表单块
6. 性能友好子表单的上下文是静态的,不会引起多余重渲染大表单拆分后性能反而更好

典型使用场景

代码示例(最常见的几种用法)

1、基础用法(最常用)

withForm 最常用的几个参数及其详细讲解:

参数名类型是否必须主要作用实际使用场景 & 注意事项
defaultValuesobject / () => object推荐只用于类型推导,定义子表单的字段结构和类型,帮助 TypeScript 推导字段名和值类型必须写完整结构(不管有多少层子表单嵌套,都是最终的那个表单的类型),但运行时不生效(实际值来自父表单)。常用于字段名自动补全和类型安全。
propsobject (类型定义)可选定义组件接收的额外 props 类型(如 title、className 等),增强组件可配置性让子表单组件更灵活,例如
renderfunction({ form, props }) => JSX必须实际渲染函数,接收子表单的 form(FormApi 实例)和外部 props,返回 JSX这里写真正的 UI 和 <form.AppField> 使用逻辑。必须是命名函数(function Render() {}),避免 ESLint hooks 规则报错。
fieldsobject (运行时传入)必须(当有字段映射时)字段路径映射,把父表单的深层字段映射到子表单的局部字段名核心功能!例如 { street: 'shipping.street' },子表单写 street 就映射到父级的 shipping.street
validatorAdapterValidatorAdapter可选子表单专属的校验适配器(通常继承父表单)极少单独指定,大多数情况继承全局的(如 zodValidator)
onSubmit(values) => Promise可选子表单独立的提交处理器很少用,通常由父表单统一处理。适合特殊子表单需要独立提交的场景
transform(values) => any可选在子表单提交前对值进行转换(类似 schema transform)例如把局部值合并回父表单结构,或格式化数据

重点就是defaultValues和render参数。

2、嵌套使用(子表单里再套子表单)

3、动态添加子表单(常见于“添加家庭成员”)

重点注意事项

老师案例

案例中,skills部分的代码比较多,将这部分拆分为子表单。

1、创建一个子表单组件

其实很简单,就是使用withForm,将之前定义的registerFormOpts拿过来,然后在render里面将form.Field相关的拿过来即可。

2、使用子表单

引入之后,直接使用,指定form属性即可。

image-20251228183420244

是不是感觉很简单。

Creating Reusable Group of Fields with withFieldGroup HOC

在withForm的例子中,我们可以将一些filed放到一个子表单里面去。但是这个子表单的defaultValues属性(使用的是父表单的defaultValues)决定了,这个子表单只能用于一个具体的父表单,因为表单不可能defaultValues都相同(这么多状态变量),这可以理解吧。

withFieldGroup → 更轻量,只处理一小组字段,不创建独立 FormApi,完全共享父表单的上下文和状态。可以解决“密码确认”“日期范围”“地址三连”等强耦合字段组在多个表单/页面重复出现时的代码重复问题。

1、定义 fieldGroup 的schema

2、使用 withFieldGroup 定义组件

重点就是defaultValues和render函数。

3、修改父表单的schema,使用and拼接 fieldGroup 的schema

image-20251228191552172

注意:只是父表单里面的schema使用and拼接,父表单的默认值里面还是需要定义相关的变量的。因为在使用 fieldGroup的时候,要制定fields参数的。

4、使用组件

直接使用即可,注意设置form,就是父组件的form。以及重点:设置 fields 映射。

例子:

image-20251228191309426

可以看到,表单正常使用。

 

最佳实践: